package com.glodon.paas.account.sdk.oauth2;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.codec.Base64;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.provider.BaseClientDetails;
import org.springframework.security.oauth2.provider.DefaultAuthorizationRequest;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;

import java.io.UnsupportedEncodingException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.isNullOrEmpty;

/**
 * Verify oauth2 access token remotely with the authentication server
 *
 * @author Don Li
 */
public class RemoteTokenService implements ResourceServerTokenServices {

    private static final Logger LOGGER = LoggerFactory.getLogger(RemoteTokenService.class);

    private String checkTokenEndpointUrl;

    private String clientId;

    private String clientSecret;

    private RestOperations restTemplate = new RestTemplate();

    public void setCheckTokenEndpointUrl(String checkTokenEndpointUrl) {
        this.checkTokenEndpointUrl = checkTokenEndpointUrl;
    }

    public void setClientId(String clientId) {
        this.clientId = clientId;
    }

    public void setClientSecret(String clientSecret) {
        this.clientSecret = clientSecret;
    }

    @Override
    public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException {
        if (isNullOrEmpty(accessToken)) {
            return null;
        }

        // send request to provider for token authentication
        MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
        formData.add("token", accessToken);
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", getAuthorizationHeader(clientId, clientSecret));
        Map<String, Object> map = postForMap(checkTokenEndpointUrl, formData, headers);

        if (map.containsKey("error")) {
            LOGGER.info("check_token returned error: {}", map.get("error"));
            throw new InvalidTokenException(accessToken);
        }

        String remoteClientId = (String) map.get("client_id");
        checkNotNull(remoteClientId);

        Set<String> scope = new HashSet<String>();
        if (map.containsKey("scope")) {
            @SuppressWarnings("unchecked")
            Collection<String> values = (Collection<String>) map.get("scope");
            scope.addAll(values);
        }

        DefaultAuthorizationRequest clientAuthentication = new DefaultAuthorizationRequest(remoteClientId, scope);

        if (map.containsKey("resource_ids") || map.containsKey("client_authorities")) {
            Set<String> resourceIds = new HashSet<String>();
            if (map.containsKey("resource_ids")) {
                @SuppressWarnings("unchecked")
                Collection<String> values = (Collection<String>) map.get("resource_ids");
                resourceIds.addAll(values);
            }
            Set<GrantedAuthority> clientAuthorities = new HashSet<GrantedAuthority>();
            if (map.containsKey("client_authorities")) {
                @SuppressWarnings("unchecked")
                Collection<Map<String, ?>> values = (Collection<Map<String, ?>>) map.get("client_authorities");
                clientAuthorities.addAll(getAuthorities(values));
            }
            BaseClientDetails clientDetails = new BaseClientDetails();
            clientDetails.setClientId(remoteClientId);
            clientDetails.setResourceIds(resourceIds);
            clientDetails.setAuthorities(clientAuthorities);
            clientAuthentication.addClientDetails(clientDetails);
        }

        Set<GrantedAuthority> userAuthorities = new HashSet<GrantedAuthority>();
        if (map.containsKey("user_authorities")) {
            @SuppressWarnings("unchecked")
            Collection<Map<String, ?>> values = (Collection<Map<String, ?>>) map.get("user_authorities");
            userAuthorities.addAll(getAuthorities(values));
        }
        String uid = (String) map.get("user_id");
        Authentication userAuthentication = null;
        if (uid != null) {
            userAuthentication = new UsernamePasswordAuthenticationToken(uid, null, userAuthorities);
        }

        clientAuthentication.setApproved(true);
        return new OAuth2Authentication(clientAuthentication, userAuthentication);
    }

    @Override
    public OAuth2AccessToken readAccessToken(String accessToken) {
        throw new UnsupportedOperationException("Not supported: read access token");
    }

    private String getAuthorizationHeader(String clientId, String clientSecret) {
        String creds = String.format("%s:%s", clientId, clientSecret);
        try {
            return "Basic " + new String(Base64.encode(creds.getBytes("UTF-8")));
        } catch (UnsupportedEncodingException e) {
            throw new IllegalStateException("Could not convert String");
        }
    }

    private Map<String, Object> postForMap(String path, MultiValueMap<String, String> formData, HttpHeaders headers) {
        if (headers.getContentType() == null) {
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        }
        @SuppressWarnings("rawtypes")
        Map map = restTemplate.exchange(path, HttpMethod.POST,
                new HttpEntity<MultiValueMap<String, String>>(formData, headers), Map.class).getBody();
        @SuppressWarnings("unchecked")
        Map<String, Object> result = (Map<String, Object>) map;
        return result;
    }

    private Set<GrantedAuthority> getAuthorities(Collection<Map<String, ?>> authorities) {
        Set<GrantedAuthority> result = new HashSet<GrantedAuthority>();
        for (Map authority : authorities) {
            result.add(new SimpleGrantedAuthority((String) authority.get("authority")));
        }
        return result;
    }
}
